//  Copyright (c) Microsoft Corporation.  All Rights Reserved.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.IO;
using System.Xml;

using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Security;
using System.ServiceModel.Security.Tokens;

using System.IdentityModel;
using System.IdentityModel.Tokens;
using System.IdentityModel.Policy;

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;

using EAI.Common;
using EAI.SAML;

using SigningCredentials = EAI.SAML.SigningCredentials;

namespace EAI.STS
{
    [ServiceContract]
    public interface IEAISecurityTokenService
    {
        [OperationContract(Action = Constants.RequestSecurityTokenAction,
                           ReplyAction = Constants.RequestSecurityTokenResponseAction)]
        Message ProcessRequestSecurityToken(Message rstMessage);
    }


    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class EAISecurityTokenService : IEAISecurityTokenService
    {
        public const string SamlIssuer = "urn:microsoft.wcf.samples.ServiceSTS";
        public static readonly Uri SamlAudienceUri = new Uri("uri:someuri");
        X509SecurityToken serverToken;

        public EAISecurityTokenService()
        {
            serverToken = new X509SecurityToken(Utils.GetCertificate(StoreName.My, StoreLocation.LocalMachine, X509FindType.FindBySubjectName, "WCFService"));
        }

        SigningCredentials GetIssuerSigningCredentials()
        {
            X509SecurityToken stsToken = new X509SecurityToken(Utils.GetCertificate(StoreName.My, StoreLocation.LocalMachine, X509FindType.FindBySubjectName, "ServiceSTS"));
            SecurityKeyIdentifier securityKeyIdentifier = new SecurityKeyIdentifier();
            securityKeyIdentifier.Add(stsToken.CreateKeyIdentifierClause<X509ThumbprintKeyIdentifierClause>());

            SigningCredentials signingCredentials = new SigningCredentials(
                                stsToken.SecurityKeys[0],
                                SecurityAlgorithms.RsaSha1Signature,
                                SecurityAlgorithms.Sha1Digest,
                                securityKeyIdentifier);
            return signingCredentials;
        }

        SecurityToken CreateSymmetricSamlToken(BinarySecretSecurityToken proofToken)
        {
            byte[] proofKey = proofToken.GetKeyBytes();
            SecurityKeyIdentifierClause skic = serverToken.CreateKeyIdentifierClause<X509ThumbprintKeyIdentifierClause>();
            SecurityKeyIdentifier securityEncryptionKeyIdentifier = new SecurityKeyIdentifier(skic);
            byte[] wrappedKey = serverToken.SecurityKeys[0].EncryptKey(SecurityAlgorithms.RsaOaepKeyWrap, proofKey);
            SecurityKeyIdentifier skiSymmetric = new SecurityKeyIdentifier();
            EncryptedKeyIdentifierClause encryptedKeyClause = new EncryptedKeyIdentifierClause(wrappedKey, SecurityAlgorithms.RsaOaepKeyWrap, securityEncryptionKeyIdentifier);
            skiSymmetric.Add(encryptedKeyClause);

            EAISamlOneTimeUse condition = new EAISamlOneTimeUse();
            EAISamlConditions samlConditions = new EAISamlConditions();
            samlConditions.NotBefore = DateTime.UtcNow;
            samlConditions.NotOnOrAfter = DateTime.UtcNow.AddDays((double)1);
            samlConditions.Conditions.Add(condition);

            EAISamlSubjectConfirmationData confirmationData = new EAISamlSubjectConfirmationData();
            confirmationData.NotBefore = DateTime.Now;
            confirmationData.NotAfter = DateTime.Now.AddDays((double)1);
            confirmationData.Address = System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())[0].ToString();
            confirmationData.Recipient = SamlAudienceUri;
            confirmationData.KeyInfos.Add(skiSymmetric);
            EAISamlSubjectConfirmation confirmation = new EAISamlSubjectConfirmation(new Uri(EAISamlConstants.HolderOfKey));
            confirmation.SubjectConfirmationData = confirmationData;
            EAISamlSubject subject = new EAISamlSubject();
            subject.SubjectConfirmations.Add(confirmation);

            EAISamlAttributeStatement samlAttributeStatement = new EAISamlAttributeStatement();
            EAISamlAttribute samlAttribute1 = new EAISamlAttribute();
            samlAttribute1.Name = EAISamlConstants.UserName;
            samlAttribute1.NameFormat = EAISamlConstants.UserNameNamespace;
            samlAttribute1.AttributeValues.Add("alice");
            samlAttributeStatement.Attributes.Add(samlAttribute1);
            EAISamlAttribute samlAttribute2 = new EAISamlAttribute();
            samlAttribute2.NameFormat = EAISamlConstants.EmailNamespace;
            samlAttribute2.Name = EAISamlConstants.EmailName;
            samlAttribute2.AttributeValues.Add("alice@contoso.com");
            samlAttribute2.AttributeValues.Add("alice_crocker@hotmail.com");
            samlAttributeStatement.Attributes.Add(samlAttribute2);

            SigningCredentials signingCredentials = GetIssuerSigningCredentials();
            EAISamlAssertion samlAssertion = new EAISamlAssertion();
            samlAssertion.Issuer = System.Net.Dns.GetHostName();
            samlAssertion.Subject = subject;
            samlAssertion.Conditions = samlConditions;
            samlAssertion.SigningCredentials = signingCredentials;
            samlAssertion.Statements.Add(samlAttributeStatement);

            EncryptionParameters encryptionParameter = new EncryptionParameters(SecurityAlgorithms.Aes256Encryption, 32, serverToken, new SecurityKeyIdentifier(serverToken.CreateKeyIdentifierClause<X509ThumbprintKeyIdentifierClause>()), EncryptedXml.XmlEncRSA15Url);

            EAISamlEncryptedAssertion encSamlAssertion = new EAISamlEncryptedAssertion(samlAssertion, encryptionParameter);

            return new EAISamlSecurityToken(encSamlAssertion);
        }

        public Message ProcessRequestSecurityToken(Message rstMessage)
        {
            RequestSecurityToken rst = RequestSecurityToken.CreateFrom(rstMessage.GetReaderAtBodyContents());
            SecurityToken samlToken;
            SecurityToken proofToken;
            byte[] senderEntropy = null;
            byte[] stsEntropy = null;

            EndpointAddress10 epr = rst.GetAppliesTo<EndpointAddress10>();
            if (epr == null)
            {
                throw new Exception("The message does not have an EndpointAddress EPR");
            }
            Console.WriteLine(String.Format("Request has EndpointAddress AppliesTo {0}", epr.ToEndpointAddress().Uri.ToString()));

            int keySize;
            if (rst.KeySize != 0)
                keySize = rst.KeySize;
            else
                keySize = 256;
            SecurityToken entropyToken = rst.GetRequestorEntropy();
            if (entropyToken != null)
            {
                senderEntropy = ((BinarySecretSecurityToken)entropyToken).GetKeyBytes();
            }
            byte[] key;
            if (senderEntropy != null)
            {
                stsEntropy = new byte[keySize / 8];
                RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();
                random.GetNonZeroBytes(stsEntropy);
                key = RequestSecurityTokenResponse.ComputeCombinedKey(senderEntropy, stsEntropy, keySize);
            }
            else
            {
                key = new byte[keySize / 8];
                RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();
                random.GetNonZeroBytes(key);
            }
            proofToken = new BinarySecretSecurityToken(key);

            samlToken = CreateSymmetricSamlToken((BinarySecretSecurityToken)proofToken);

            RequestSecurityTokenResponse rstr = new RequestSecurityTokenResponse(new CustomTokenSerializer(SecurityVersion.WSSecurity11, new EAISamlSerializer()));
            // The SAML token inside the RSTR is not encrypted. When the client presents this to the Service,
            // the token will be unencrypted on the wire. To avoid this you can encrypt the SAML token using 
            // the service certificate and create a Generic XML Security Token using the encrypted .
            rstr.RequestedSecurityToken = samlToken;
            if (senderEntropy != null)
            {
                rstr.SetIssuerEntropy(stsEntropy);
                rstr.ComputeKey = true;
            }
            else
            {
                rstr.RequestedProofToken = proofToken;
            }
            rstr.SetLifetime(DateTime.UtcNow, DateTime.UtcNow.Add(TimeSpan.FromDays(1)));
            rstr.RequestedAttachedReference = samlToken.CreateKeyIdentifierClause<EAISamlAssertionKeyIdentifierClause>();
            rstr.RequestedUnattachedReference = samlToken.CreateKeyIdentifierClause<EAISamlAssertionKeyIdentifierClause>();
            rstr.MakeReadOnly();

            Message rstrMessage = Message.CreateMessage(rstMessage.Version, Constants.RequestSecurityTokenResponseAction, rstr);

            UniqueId requestMessageID = rstMessage.Headers.MessageId;
            if (requestMessageID == null)
                throw new InvalidOperationException("The request message does not have a message ID.");
            rstrMessage.Headers.RelatesTo = requestMessageID;

            Console.WriteLine("Serivce STS issues SAML token.");

            return rstrMessage;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost serviceHost = new ServiceHost(typeof(EAISecurityTokenService));

            // WARNING: The below setting is not a secure options. The sample uses these
            // as the certificates used by this sample are created by makecert which does not
            // have a valid chain trust and is issued by a test root. Production code should
            // not use these settings.
            serviceHost.Credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;

            serviceHost.Open();

            Console.WriteLine("Press [Enter] to terminate Service EAI.STS.");
            Console.ReadLine();

            serviceHost.Close();
        }
    }
}
